import { hasPerm } from '@/config/acl'
import db from './db'
import { removeDoc, upload } from './remote'
import { store } from './store'
import path, { resolve } from 'path'
import fs from 'fs'
import { access } from './perms'
import { sha } from './md5'
import Vue from 'vue'
import chokidar from 'chokidar'
import { debounce, reject } from 'lodash'
import { getDocById, getDocByPath, getUserDocs } from '@/api/doc'
import { download, DownloadItem } from './download'
import { getLastOne } from '@/api/doc-action'

async function readAllFilesInDir (p: string, store: Set<string>, cb?: () => void, updateFn?: (num: number) => void) {
  const handler = () => new Promise((resolve, reject) => {
    const run = () => {
      const files = fs.readdirSync(p)
      const all = files.map((file) => {
        return new Promise((resolve, reject) => {
          try {
            const filePath = path.join(p, file)
            const stats = fs.statSync(filePath)
            store.add(filePath)
            if (store.size % 5000 === 0) {
              if (updateFn) updateFn(store.size)
            }
            if (stats.isDirectory()) {
              // 递归调用读取子文件夹中的文件
              readAllFilesInDir(filePath, store, undefined, updateFn).then(() => {
                resolve(null)
              })
            } else {
              resolve(null)
            }
          } catch (e) {
            console.error(e)
            resolve(null)
          }
        })
      })
      Promise.all(all).then(() => {
        resolve(null)
      })
    }
    setTimeout(() => {
      run()
    }, 50)
  })
  await handler()
  if (cb) {
    cb()
  }
}

export class Driver {
  localPath!: string;

  docId!: number;

  type!: 'up' | 'down' | 'duplex';

  name!: string;

  status?: 'updating' | 'error' | 'completed' | 'pause';

  updateTime?: number = null as any;

  period = 10;

  id = Date.now();

  spaceId!: number;

  message = '';

  total = 0;

  progress = 0;

  updatingInfo = {
    path: '',
    size: 0,
    progress: 0
  }

  uid = 0

  disabledListen = false // 禁用监听
}

export interface FileChangeLog {
  driverId: number;

  path: string;

  name: string;

  dir: boolean;

  sha: string;

  modifiedTime: number;
}

export interface LocalFile {
  name: string;

  path: string;

  size: number;

  sha: string

  dir: boolean

  modifiedTime: number;

  action: 'add' | 'remove';
}

export interface DownItem {
  type: 'create' | 'update' | 'rename' | 'delete'

  data: any

  uid: number

  oldPath?: string
}

class Obs {
  files: Set<string> = new Set()

  syncFiles: Set<string> = new Set()

  bathPath!: string

  notify!: any

  watcher: any

  disabledListen?: boolean

  updateFn?: (num: number) => void

  constructor (bathPath: string, fn: () => void, disabledListen?: boolean, updateFn?: (num: number) => void) {
    this.bathPath = bathPath
    this.disabledListen = disabledListen
    this.updateFn = updateFn
    this.notify = debounce(() => fn(), 350)
    this.watch()
  }

  watch () {
    // let idx = 0
    // this.watcher = chokidar.watch(this.bathPath).on('all', (event, path) => {
    //   if (['add', 'change', 'unlink'].includes(event)) {
    //     console.log(idx, path)
    //     idx++
    //     // this.notify()
    //     // this.files.add(path)
    //   }
    // })
    if (!this.disabledListen) {
      this.watcher = chokidar.watch(this.bathPath).on('all', (event, path) => {
        if (['add', 'change', 'unlink'].includes(event)) {
          this.notify()
          this.files.add(path)
        }
      })
    } else {
      const files = new Set<string>()
      readAllFilesInDir(this.bathPath, files, () => {
        this.files = files
        this.notify()
      }, this.updateFn)
    }
  }

  close () {
    if (this.watcher) this.watcher.close()
  }

  fetchFiles () {
    this.syncFiles = new Set([...this.files, ...this.syncFiles])
    const ret = Array.from(this.syncFiles)
    this.files = new Set()
    return ret
  }
}
class Downer {
  driver: Driver

  syncItem: DownItem | null = null

  stats = ((ctx) => {
    return new Vue({
      data: {
        syncedItems: [] as any[],
        loadedItems: [] as any[]
      },
      watch: {
        syncedItems: function () {
          ctx.driver.progress = this.syncedItems.length
        },
        loadedItems: function () {
          ctx.driver.total = this.loadedItems.length
        }
      }
    })
  })(this)

  currentItems: DownItem[] = []

  rootDoc: any

  fn!: any

  timer: any

  constructor (driver: Driver, fn: () => void, force?: boolean) {
    this.driver = driver
    this.fn = fn

    this.start(force)
  }

  notify () {
    if (this.fn) this.fn()
  }

  fetchFiles (): Promise<DownItem[]> {
    if (this.currentItems.length < 0) return Promise.resolve([])
    else {
      const ret: DownItem[] = []

      const handler: () => Promise<any> = () => {
        if (ret.length > 100) return Promise.resolve(null)
        if (this.currentItems.length < 1) return Promise.resolve(null)

        const item = this.currentItems.pop() as DownItem

        if (['rename', 'delete'].includes(item.type)) {
          ret.push(item)
          this.stats.loadedItems.push(item)
          return handler()
        } else if (['create', 'update'].includes(item.type)) {
          if (item.data?.type !== 'dir') {
            if (hasPerm('download', item.data)) {
              ret.push(item)
              this.stats.loadedItems.push(item)
            }
            return handler()
          } else {
            if (hasPerm('download', item.data)) {
              return getUserDocs({ page: 0, size: 10000 }, item.data?.id).then((res: any) => {
                const rows: any[] = res?.content || []
                rows.forEach(row => {
                  if (hasPerm('download', row)) {
                    if (row.type === 'dir') {
                      this.currentItems.push({ type: 'create', data: row, uid: this.syncItem?.uid as any })
                    } else {
                      ret.push({ type: 'create', data: row, uid: this.syncItem?.uid as any })
                      this.stats.loadedItems.push(item)
                    }
                  }
                })
                return handler()
              }).catch(() => {
                handler()
              })
            } else {
              return handler()
            }
          }
        } else {
          throw new Error('不支持同步类型: ' + item.type)
        }
      }

      return handler().then(() => {
        return ret
      })
    }
  }

  fire (syncItem: DownItem) {
    if (this.driver.status === 'updating') return
    this.stats.syncedItems = []
    this.stats.loadedItems = []
    this.syncItem = syncItem
    this.currentItems = [syncItem]

    this.driver.uid = syncItem.uid

    // this.driver.status = 'updating'

    this.notify()
  }

  start (force = false) {
    if (!force && this.driver.status === 'error') return
    if (this.driver.status === 'updating') return

    const handler = () => {
      if (!this.driver.uid || force) {
        const item: DownItem = {
          uid: Date.now(),
          type: 'create',
          data: this.rootDoc
        }
        this.fire(item)
      }
      if (this.timer) clearInterval(this.timer)
      this.timer = setInterval(() => {
        if (['updating', 'error'].includes(this.driver.status as any)) return
        const p = {
          path: this.rootDoc?.path,
          version: this.driver.uid
        }
        getLastOne(p).then(data => {
          if (data) {
            const item: DownItem = {
              uid: data.version,
              type: data?.type,
              data: data.docDTO,
              oldPath: data.oldPath
            }
            this.fire(item)
          }
        })
      }, 5 * 1000)
    }

    if (!this.rootDoc) {
      getDocById(this.driver.docId).then(data => {
        this.rootDoc = data || null
        if (!this.rootDoc) {
          return Promise.reject(new Error('文档: ' + this.driver.docId + ' 不存在'))
        }
        handler()
      }).catch(e => {
        this.errorHandler(e)
      })
    } else {
      handler()
    }
  }

  errorHandler (e: any) {
    this.driver.status = 'error'
    this.driver.message = e?.message || e
  }

  abort () {
    if (this.timer) clearInterval(this.timer)
  }
}

const obsMap = new Map<number, Obs>()
const downerMap = new Map<number, Downer>()

export const abort = () => {
  store.drivers.forEach(row => {
    if (row.status === 'updating') {
      row.status = 'error'
      row.message = '手动终止'
    }
    const obs = obsMap.get(row.id)
    if (obs) {
      obsMap.delete(row.id)
      obs.close()
    }
  })
}

export const init = () => {
  store.reloadDrivers()
  store.drivers.forEach(row => {
    if (row.type === 'up') {
      if (row.status === 'updating' || row.status === 'error') return
      push(row.id)
    } else if (row.type === 'down') {
      if (row.status === 'updating' || row.status === 'error') return
      pull(row.id)
    }
  })
}

export function push (driverId: number): Promise<any> {
  const driver: Driver = store.drivers.find(v => v.id === driverId) as any
  if (!driver) {
    return Promise.reject(new Error('同步盘不存在'))
  }
  if (driver.status === 'updating') return Promise.resolve()
  Vue.set(driver, 'updatingInfo', {
    path: '',
    size: 0,
    progress: 0
  })
  if (!obsMap.get(driverId)) {
    const updateInitInfo = (num = 0) => {
      driver.updatingInfo = {
        size: 0,
        progress: -1,
        path: '初始化监听: ' + ` (${num}); ` + driver.localPath
      }
    }
    updateInitInfo(0)
    obsMap.set(driverId, new Obs(driver.localPath, () => push(driverId), driver.disabledListen, updateInitInfo))
    driver.status = '' as any
    return Promise.resolve()
  }
  const p = driver.localPath
  const diffFiles: LocalFile[] = []
  const uid = Date.now()
  driver.uid = uid
  return access(driver.docId, 'upload', 'delete').then(has => {
    if (has) {

    } else {
      throw new Error('无同步上传权限，请联系管理员赋权')
    }
  }).then(() => {
    driver.status = 'updating'
    driver.message = ''
    driver.updatingInfo = {
      size: 0,
      progress: -1,
      path: ''
    }
    return computeDiffFiles().then(() => {
      driver.total = diffFiles.length
      driver.progress = 0
      return schedule(diffFiles, driver, uid).then(() => {
        const obs = obsMap.get(driverId)
        if (obs) obs.syncFiles.clear()
        driver.status = 'completed'
        driver.updateTime = Date.now()
        if (obs && obs.files.size > 0) {
          push(driverId)
        }
      })
    })
    // return subPush(p).then(() => {
    //   driver.total = diffFiles.length
    //   driver.progress = 0
    //   return schedule(diffFiles, driver, uid).then(() => {
    //     driver.status = 'completed'
    //     driver.updateTime = Date.now()
    //   })
    // })
  }).catch(err => {
    console.error(err)
    errorHandler(typeof err === 'string' ? err : (err?.message || err))
  }).finally(() => {
    store.saveDrivers()
  })

  function subPush (p: string): Promise<any> {
    driver.updatingInfo = {
      size: diffFiles.length,
      progress: -1,
      path: p
    }
    if (driver.status === 'error' || driver.uid !== uid) {
      throw new Error('force stop')
    }
    return Promise.all([listFiles(p, driver), listChangeLogs(driverId, p)]).then(([files, logs]) => {
      const map: Map<string, FileChangeLog> = new Map(logs.map(v => [v.name, v]))
      const all = files.map(file => {
        if (driver.status === 'error' || driver.uid !== uid) {
          throw new Error('force stop')
        }
        if (file.dir) {
          return subPush(path.resolve(file.path, file.name))
        } else {
          const log = map.get(file.name)
          if (diff(file, log)) {
            // return upload(file, driver.docId, driver.localPath).then(() => {
            //   return updateChangeLog(driverId, file)
            // })
            // console.log('000', file.path)
            diffFiles.push(file)
            return Promise.resolve()
          } else {
            return Promise.resolve()
          }
        }
      })
      return Promise.all(all)
    })
  }

  function computeDiffFiles (): Promise<any> {
    const obs = obsMap.get(driverId)
    if (!obs) return Promise.resolve(null)
    const filePaths = obs.fetchFiles()
    if (filePaths.length < 1) return Promise.resolve(null)
    const files: LocalFile[] = []
    return scheduleGenLocalFiles(filePaths).then(() => {
      return genDiffFiles(files)
    })

    function scheduleGenLocalFiles (paths: string[]): Promise<any> {
      if (paths.length < 1) return Promise.resolve()
      // const num = Math.ceil(filePaths.length / 20) // 分20份
      // const c = paths
      // const ret = paths.slice(num)
      const updateInfo = () => {
        driver.updatingInfo = {
          size: filePaths.length,
          progress: -1,
          path: '读取文件: ' + Number(files.length / filePaths.length * 100).toFixed(2) + '%'
        }
      }
      updateInfo()
      const max = 5000
      let idx = 0
      const doRun = (): Promise<any> => {
        const c = paths.slice(idx, idx + max)
        if (c.length < 1) return Promise.resolve(null)
        idx = idx + c.length
        updateInfo()
        return Promise.all(c.map(v => {
          return genLocalFile(v).then(r => {
            files.push(r)
            return r
          })
        })).then(() => {
          return doRun()
        })
      }
      return doRun()

      function genLocalFile (p: string): Promise<LocalFile> {
        return new Promise((resolve, reject) => {
          const run = () => {
            fs.stat(p, (err, stats) => {
              if (err) {
                if (err?.code === 'ENOENT') {
                  resolve({
                    name: path.basename(p),
                    path: path.dirname(p),
                    size: 0,
                    sha: '',
                    dir: false,
                    modifiedTime: 0,
                    action: 'remove'
                  })
                } else {
                  console.error(err)
                  reject(err)
                }
              } else {
                resolve({
                  name: path.basename(p),
                  path: path.dirname(p),
                  size: stats.size,
                  sha: sha(stats),
                  dir: stats.isDirectory(),
                  modifiedTime: stats.mtime.getTime(),
                  action: 'add'
                })
              }
            })
          }
          setTimeout(() => {
            run()
          }, 10)
        })
      }
    }

    function genDiffFiles (files: LocalFile[]): Promise<any> {
      return new Promise((resolve, reject) => {
        driver.updatingInfo = {
          size: diffFiles.length,
          progress: -1,
          path: '对比文件: ' + diffFiles.length + ' / ' + files.length + '(' + Number(0).toFixed(2) + '%)'
        }
        db.find({ driverId }, (err: any, docs: FileChangeLog[]) => {
          if (err) reject(err)
          const map: Map<string, FileChangeLog> = new Map(docs.map(v => [path.resolve(v.path, v.name), v]))
          files.forEach((v, i) => {
            if (diff(v, map.get(path.resolve(v.path, v.name)))) {
              diffFiles.push(v)
            }
          })
          driver.updatingInfo = {
            size: diffFiles.length,
            progress: -1,
            path: '对比文件: ' + diffFiles.length + ' / ' + files.length + '(' + Number(100).toFixed(2) + '%)'
          }
          resolve(null)
        })
      })
    }
  }

  function errorHandler (msg: string) {
    driver.message = msg || '未知错误'
    driver.updateTime = Date.now()
    driver.status = 'error'
  }
}

export function pull (driverId: number, force?: boolean): Promise<any> {
  const driver: Driver = store.drivers.find(v => v.id === driverId) as any
  if (!driver) {
    return Promise.reject(new Error('同步盘不存在'))
  }
  if (driver.status === 'updating') return Promise.resolve()
  Vue.set(driver, 'updatingInfo', {
    path: '',
    size: 0,
    progress: 0
  })
  if (!downerMap.get(driverId)) {
    driver.updatingInfo = {
      size: 0,
      progress: -1,
      path: '初始化: ' + driver.localPath
    }
    downerMap.set(driverId, new Downer(driver, () => pull(driverId), force))
    driver.status = '' as any
    return Promise.resolve()
  } else {
    if (force) {
      const r = downerMap.get(driverId)
      if (r) {
        r.start(true)
        return Promise.resolve()
      }
    }
  }

  driver.status = 'updating'

  return subPull().then(() => {
    driver.status = 'completed'
    driver.updateTime = Date.now()
  }).catch((err: any) => {
    errorHandler(typeof err === 'string' ? err : (err?.message || err))
  }).finally(() => {
    store.saveDrivers()
  })

  function subPull (): Promise<any> {
    const downer = downerMap.get(driverId) as Downer
    if (!downer) Promise.resolve()
    return downer.fetchFiles().then(items => {
      if (items.length < 1) return Promise.resolve()
      const all = items.map(item => {
        if (item.type === 'delete') {
          return doDelete(item, downer).then(() => {
            downer.stats.syncedItems.push(item)
            driver.updatingInfo = {
              size: item.data?.size,
              progress: -1,
              path: '删除: ' + item.data?.path
            }
          })
        } else if (item.type === 'rename') {
          return doRename(item, downer).then(() => {
            downer.stats.syncedItems.push(item)
            driver.updatingInfo = {
              size: item.data?.size,
              progress: -1,
              path: '重命名: ' + item.data?.path
            }
          })
        } else if (['create', 'update'].includes(item.type)) {
          return doDownload(item, downer).then(() => {
            downer.stats.syncedItems.push(item)
            driver.updatingInfo = {
              size: item.data?.size,
              progress: 1,
              path: '下载: ' + item.data?.path
            }
          })
        } else {
          return Promise.resolve()
        }
      })
      return Promise.all(all as any).then(() => {
        return subPull()
      })
    })
  }

  function doDelete (item: DownItem, downer: Downer) {
    const p = resolveLocalPath(item, downer)
    del(p)
    return Promise.resolve()

    function del (p: string) {
      try {
        if (fs.statSync(p).isFile()) {
          fs.unlinkSync(p)
          return
        }
        let files = []
        if (fs.existsSync(p)) {
          files = fs.readdirSync(p)
          files.forEach((file, index) => {
            const curPath = path.resolve(p, file)
            if (fs.statSync(curPath).isDirectory()) {
              del(curPath) // 递归删除文件夹
            } else {
              fs.unlinkSync(curPath) // 删除文件
            }
          })
          fs.rmdirSync(p) // 删除文件夹自身
        }
      } catch (e) {
      }
    }
  }

  function doRename (item: DownItem, downer: Downer) {
    try {
      const p = resolveLocalPath(item, downer)
      const o = path.resolve(p, '..', path.basename(item.oldPath || ''))
      fs.renameSync(o, p)
    } catch (error) {
    }
    return Promise.resolve()
  }

  function doDownload (item: DownItem, downer: Downer) {
    const p = resolveLocalPath(item, downer)
    const params = new DownloadItem(item.data?.url, p, item.data?.name, item.data?.size)
    return download(params, downer.driver, () => {
      driver.updatingInfo = {
        size: item.data?.size,
        progress: params.done / params.size,
        path: '下载: ' + item.data?.path
      }
    })
  }

  function resolveLocalPath (item: DownItem, downer: Downer) {
    const b = driver.localPath
    const r = downer.rootDoc?.path
    const p = (item.type === 'delete' && item.oldPath) ? item.oldPath : (item.data?.path || '')
    return path.resolve(b, '.' + p.replace(r, ''))
  }

  function errorHandler (msg: string) {
    driver.message = msg || '未知错误'
    driver.updateTime = Date.now()
    driver.status = 'error'
  }
}

function listFiles (p: string, driver?: Driver): Promise<LocalFile[]> {
  return new Promise<string[]>((resolve, reject) => {
    fs.readdir(p, (err, files) => {
      if (err) reject(err)
      else resolve(files)
    })
  }).then((files: string[]) => {
    const all = files.map(v => {
      return new Promise<any>((resolve, reject) => {
        fs.stat(path.resolve(p, v), (err, stats) => {
          if (err) reject(err)
          else {
            resolve({
              name: v,
              path: p,
              size: stats.size,
              sha: sha(stats),
              dir: stats.isDirectory(),
              modifiedTime: stats.mtime.getTime()
            })
          }
        })
        if (driver?.status === 'error') {
          throw new Error('force stop')
        }
      })
    })
    return Promise.all(all)
  })
}

function listChangeLogs (driverId: number, path: string): Promise<FileChangeLog[]> {
  return new Promise((resolve, reject) => {
    db.find({ driverId, path }, (err: any, docs: any[]) => {
      if (err) reject(err)
      else resolve(docs)
    })
  })
}

function diff (file: LocalFile, log?: FileChangeLog): boolean {
  if (!log) return true
  if (file.action === 'remove') return true
  if (file.sha) {
    return file.sha !== log.sha
  }
  if (file.modifiedTime === log.modifiedTime) return false
  return true
}

function updateChangeLog (driverId: number, file: LocalFile): Promise<FileChangeLog> {
  return new Promise((resolve, reject) => {
    if (file?.action === 'remove') {
      db.remove({ driverId, path: file?.path, name: file?.name }, (err: any) => {
        if (err) reject(err)
        else {
          resolve(null as any)
        }
      })
    } else {
      const log = {
        driverId: driverId,
        path: file.path,
        name: file.name,
        dir: file.dir,
        sha: file.sha,
        modifiedTime: file.modifiedTime
      }
      db.update({ driverId, path: file.path, name: file.name }, log, { multi: true, upsert: true }, (err: any) => {
        if (err) reject(err)
        else resolve(log)
      })
    }
  })
}

function schedule (files: LocalFile[], driver: Driver, uid: number, parallel = 5): Promise<any> {
  return new Promise((resolve, reject) => {
    if (files.length < 1) resolve(null)

    const max = Math.min(parallel, files.length)
    const start = files.slice(0)
    const end: LocalFile[] = []

    for (let i = 0; i < max; i++) {
      const file = start.pop() as any
      handle(file)
    }

    function handle (file: LocalFile) {
      if (driver.status === 'error' || driver.uid !== uid) {
        throw new Error('force stop')
      }
      Vue.set(driver, 'updatingInfo', {
        path: file.path + '/' + file.name,
        size: file.size,
        progress: 0
      })
      const pfn = (client: any, p: number) => {
        driver.updatingInfo = {
          path: file.path + '/' + file.name,
          size: file.size,
          progress: p || 0
        }
        if (driver.status === 'error' && client) {
          client.cancel()
        }
      }
      const request = file?.action === 'remove' ? removeDoc(file, driver?.docId, driver.localPath) : upload(file, driver.docId, driver.localPath, pfn)
      request.then(() => {
        return updateChangeLog(driver.id, file)
      }).then(() => {
        end.push(file)
        driver.progress = end.length
        if (start.length > 0) handle(start.pop() as any)
        if (end.length >= files.length) resolve(null)
      }).catch(err => {
        reject(new Error(file?.path + '/' + file?.name + ' : ' + err?.message))
      })
    }
  })
}
